// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedMember.Local

/* PdfImage.cs --
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using System;
using System.Diagnostics;
using System.IO;

using AM;

using PdfSharpCore.Drawing;
using PdfSharpCore.Pdf.Filters;

#endregion

#nullable enable

namespace PdfSharpCore.Pdf.Advanced;

/// <summary>
/// Represents an image.
/// </summary>
public sealed partial class PdfImage
    : PdfXObject
{
    #region Construction

    /// <summary>
    /// Initializes a new instance of PdfImage from an XImage.
    /// </summary>
    public PdfImage (PdfDocument document, XImage image)
        : base (document)
    {
        Elements.SetName (Keys.Type, "/XObject");
        Elements.SetName (Keys.Subtype, "/Image");

        Image = image;

        ////// TODO: identify multiple used images. If the image already exists use the same XRef.
        ////_defaultName = PdfImageTable.NextImageName;

        switch (Image.Format.Guid.ToString ("B").ToUpper())
        {
            // Pdf supports Jpeg, therefore we can write what we've read:
            case "{B96B3CAE-0728-11D3-9D7B-0000F81EF32E}": //XImageFormat.Jpeg
                InitializeJpeg();
                break;

            // All other image formats are converted to PDF bitmaps:
            case "{B96B3CAF-0728-11D3-9D7B-0000F81EF32E}": //XImageFormat.Png
            case "{B96B3CB0-0728-11D3-9D7B-0000F81EF32E}": //XImageFormat.Gif
            case "{B96B3CB1-0728-11D3-9D7B-0000F81EF32E}": //XImageFormat.Tiff
            case "{B96B3CB5-0728-11D3-9D7B-0000F81EF32E}": //XImageFormat.Icon
                // TODO: possible optimization for PNG (do not decompress/recompress)???
                // TODO: try Jpeg for size optimization???
                InitializeNonJpeg();
                break;

            case "{84570158-DBF0-4C6B-8368-62D6A3CA76E0}": //XImageFormat.Pdf:
                Debug.Assert (false, "XPdfForm not expected here.");
                break;

            default:
                Debug.Assert (false, "Unexpected image type.");
                break;
        }
    }

    #endregion

    /// <summary>
    /// Gets the underlying XImage object.
    /// </summary>
    public XImage Image { get; }

    /// <summary>
    /// Returns 'Image'.
    /// </summary>
    public override string ToString()
    {
        return "Image";
    }

    /// <summary>
    /// Creates the keys for a JPEG image.
    /// </summary>
    void InitializeJpeg()
    {
        byte[]? imageBits;

        using (var memory = Image.AsJpeg())
        {
            imageBits = memory.ToArray();
        }

        var tryFlateDecode =
            _document!.Options.UseFlateDecoderForJpegImages == PdfUseFlateDecoderForJpegImages.Automatic;
        var useFlateDecode = _document.Options.UseFlateDecoderForJpegImages == PdfUseFlateDecoderForJpegImages.Always;

        var fd = new FlateDecode();
        var imageDataCompressed = (useFlateDecode || tryFlateDecode)
            ? fd.Encode (imageBits, _document.Options.FlateEncodeMode)
            : null;
        if (useFlateDecode || tryFlateDecode && imageDataCompressed!.Length < imageBits.Length)
        {
            Stream = new PdfStream (imageDataCompressed!, this);
            Elements[PdfStream.Keys.Length] = new PdfInteger (imageDataCompressed!.Length);
            var arrayFilters = new PdfArray (_document);
            arrayFilters.Elements.Add (new PdfName ("/FlateDecode"));
            arrayFilters.Elements.Add (new PdfName ("/DCTDecode"));
            Elements[PdfStream.Keys.Filter] = arrayFilters;
        }
        else
        {
            Stream = new PdfStream (imageBits, this);
            Elements[PdfStream.Keys.Length] = new PdfInteger (imageBits.Length);
            Elements[PdfStream.Keys.Filter] = new PdfName ("/DCTDecode");
        }

        if (Image.Interpolate)
            Elements[Keys.Interpolate] = PdfBoolean.True;
        Elements[Keys.Width] = new PdfInteger (Image.PixelWidth);
        Elements[Keys.Height] = new PdfInteger (Image.PixelHeight);
        Elements[Keys.BitsPerComponent] = new PdfInteger (8);
        Elements[Keys.ColorSpace] = new PdfName ("/DeviceRGB");
    }

    /// <summary>
    /// Creates the keys for a FLATE image.
    /// </summary>
    void InitializeNonJpeg()
    {
        ReadTrueColorMemoryBitmap (3, 8, true);
    }

    private static int ReadWord (byte[] ab, int offset)
    {
        return ab[offset] + 256 * ab[offset + 1];
    }

    private static int ReadDWord (byte[] ab, int offset)
    {
        return ReadWord (ab, offset) + 0x10000 * ReadWord (ab, offset + 2);
    }

    /// <summary>
    /// Reads images that are returned from GDI+ without color palette.
    /// </summary>
    /// <param name="components">4 (32bpp RGB), 3 (24bpp RGB, 32bpp ARGB)</param>
    /// <param name="bits">8</param>
    /// <param name="hasAlpha">true (ARGB), false (RGB)</param>
    private void ReadTrueColorMemoryBitmap (int components, int bits, bool hasAlpha)
    {
        var pdfVersion = Owner!.Version;
        var memory = Image.AsBitmap();

        // THHO4THHO Use ImageImporterBMP here to avoid redundant code.
        var streamLength = (int)memory.Length;
        Debug.Assert (streamLength > 0, "Bitmap image encoding failed.");
        if (streamLength > 0)
        {
            var imageBits = new byte[streamLength];
            memory.Seek (0, SeekOrigin.Begin);
            memory.Read (imageBits, 0, streamLength).NotUsed();
            memory.Dispose();

            var height = Image.PixelHeight;
            var width = Image.PixelWidth;

            // TODO: we could define structures for
            //   BITMAPFILEHEADER
            //   { BITMAPINFO }
            //   BITMAPINFOHEADER
            // to avoid ReadWord and ReadDWord ... (but w/o pointers this doesn't help much)

            if (ReadWord (imageBits, 0) != 0x4d42 || // "BM"
                ReadDWord (imageBits, 2) != streamLength ||
                ReadDWord (imageBits, 18) != width ||
                ReadDWord (imageBits, 22) != height)
            {
                throw new NotImplementedException ("ReadTrueColorMemoryBitmap: unsupported format");
            }

            var infoHeaderSize = ReadDWord (imageBits, 14); // sizeof BITMAPINFOHEADER
            if (infoHeaderSize != 40 && infoHeaderSize != 108)
            {
                throw new NotImplementedException ("ReadTrueColorMemoryBitmap: unsupported format #2");
            }

            var bigHeader = infoHeaderSize == 108;
            if (ReadWord (imageBits, 26) != 1 ||
                (!hasAlpha && ReadWord (imageBits, bigHeader ? 30 : 28) != components * bits ||
                 hasAlpha && ReadWord (imageBits, bigHeader ? 30 : 28) != (components + 1) * bits) ||
                bigHeader
                    ? ReadWord (imageBits, 32) != 0
                    : ReadDWord (imageBits, 30) != 0)
            {
                throw new NotImplementedException ("ReadTrueColorMemoryBitmap: unsupported format #3");
            }

            var nFileOffset = ReadDWord (imageBits, 10);
            var logicalComponents = components;
            if (components == 4)
                logicalComponents = 3;

            var imageData = new byte[components * width * height];

            var hasMask = false;
            var hasAlphaMask = false;
            var alphaMask = hasAlpha ? new byte[width * height] : null;
            var mask = hasAlpha ? new MonochromeMask (width, height) : null;

            var nOffsetRead = 0;
            if (logicalComponents == 3)
            {
                for (var y = 0; y < height; ++y)
                {
                    var nOffsetWrite = 3 * (height - 1 - y) * width;
                    var nOffsetWriteAlpha = 0;
                    if (hasAlpha)
                    {
                        mask!.StartLine (y);
                        nOffsetWriteAlpha = (height - 1 - y) * width;
                    }

                    for (var x = 0; x < width; ++x)
                    {
                        imageData[nOffsetWrite] = imageBits[nFileOffset + nOffsetRead + 2];
                        imageData[nOffsetWrite + 1] = imageBits[nFileOffset + nOffsetRead + 1];
                        imageData[nOffsetWrite + 2] = imageBits[nFileOffset + nOffsetRead];
                        if (hasAlpha)
                        {
                            mask!.AddPel (imageBits[nFileOffset + nOffsetRead + 3]);
                            alphaMask![nOffsetWriteAlpha] = imageBits[nFileOffset + nOffsetRead + 3];
                            if (!hasMask || !hasAlphaMask)
                            {
                                if (imageBits[nFileOffset + nOffsetRead + 3] != 255)
                                {
                                    hasMask = true;
                                    if (imageBits[nFileOffset + nOffsetRead + 3] != 0)
                                        hasAlphaMask = true;
                                }
                            }

                            ++nOffsetWriteAlpha;
                        }

                        nOffsetRead += hasAlpha ? 4 : components;
                        nOffsetWrite += 3;
                    }

                    nOffsetRead = 4 * ((nOffsetRead + 3) / 4); // Align to 32 bit boundary
                }
            }
            else if (components == 1)
            {
                // Grayscale
                throw new NotImplementedException ("Image format not supported (grayscale).");
            }

            var fd = new FlateDecode();
            if (hasMask)
            {
                // monochrome mask is either sufficient or
                // provided for compatibility with older reader versions
                var maskDataCompressed = fd.Encode (mask!.MaskData, _document!.Options.FlateEncodeMode);
                var pdfMask = new PdfDictionary (_document);
                pdfMask.Elements.SetName (Keys.Type, "/XObject");
                pdfMask.Elements.SetName (Keys.Subtype, "/Image");

                Owner._irefTable.Add (pdfMask);
                pdfMask.Stream = new PdfStream (maskDataCompressed, pdfMask);
                pdfMask.Elements[PdfStream.Keys.Length] = new PdfInteger (maskDataCompressed.Length);
                pdfMask.Elements[PdfStream.Keys.Filter] = new PdfName ("/FlateDecode");
                pdfMask.Elements[Keys.Width] = new PdfInteger (width);
                pdfMask.Elements[Keys.Height] = new PdfInteger (height);
                pdfMask.Elements[Keys.BitsPerComponent] = new PdfInteger (1);
                pdfMask.Elements[Keys.ImageMask] = new PdfBoolean (true);
                Elements[Keys.Mask] = pdfMask.Reference;
            }

            if (hasMask && hasAlphaMask && pdfVersion >= 14)
            {
                // The image provides an alpha mask (requires Arcrobat 5.0 or higher)
                var alphaMaskCompressed = fd.Encode (alphaMask!, _document!.Options.FlateEncodeMode);
                var smask = new PdfDictionary (_document);
                smask.Elements.SetName (Keys.Type, "/XObject");
                smask.Elements.SetName (Keys.Subtype, "/Image");

                Owner._irefTable.Add (smask);
                smask.Stream = new PdfStream (alphaMaskCompressed, smask);
                smask.Elements[PdfStream.Keys.Length] = new PdfInteger (alphaMaskCompressed.Length);
                smask.Elements[PdfStream.Keys.Filter] = new PdfName ("/FlateDecode");
                smask.Elements[Keys.Width] = new PdfInteger (width);
                smask.Elements[Keys.Height] = new PdfInteger (height);
                smask.Elements[Keys.BitsPerComponent] = new PdfInteger (8);
                smask.Elements[Keys.ColorSpace] = new PdfName ("/DeviceGray");
                Elements[Keys.SMask] = smask.Reference;
            }

            var imageDataCompressed = fd.Encode (imageData, _document!.Options.FlateEncodeMode);

            Stream = new PdfStream (imageDataCompressed, this);
            Elements[PdfStream.Keys.Length] = new PdfInteger (imageDataCompressed.Length);
            Elements[PdfStream.Keys.Filter] = new PdfName ("/FlateDecode");
            Elements[Keys.Width] = new PdfInteger (width);
            Elements[Keys.Height] = new PdfInteger (height);
            Elements[Keys.BitsPerComponent] = new PdfInteger (8);

            // TODO: CMYK
            Elements[Keys.ColorSpace] = new PdfName ("/DeviceRGB");
            if (Image.Interpolate)
                Elements[Keys.Interpolate] = PdfBoolean.True;
        }
    }

    /* BITMAPINFOHEADER struct and byte offsets:
        typedef struct tagBITMAPINFOHEADER{
          DWORD  biSize;           // 14
          LONG   biWidth;          // 18
          LONG   biHeight;         // 22
          WORD   biPlanes;         // 26
          WORD   biBitCount;       // 28
          DWORD  biCompression;    // 30
          DWORD  biSizeImage;      // 34
          LONG   biXPelsPerMeter;  // 38
          LONG   biYPelsPerMeter;  // 42
          DWORD  biClrUsed;        // 46
          DWORD  biClrImportant;   // 50
        } BITMAPINFOHEADER, *PBITMAPINFOHEADER;
    */

    private void ReadIndexedMemoryBitmap (int bits /*, ref bool hasAlpha*/)
    {
        var pdfVersion = Owner!.Version;
        int firstMaskColor = -1, lastMaskColor = -1;
        var segmentedColorMask = false;

        var memory = new MemoryStream();

        // THHO4THHO Use ImageImporterBMP here to avoid redundant code.

        var streamLength = (int)memory.Length;
        Debug.Assert (streamLength > 0, "Bitmap image encoding failed.");
        if (streamLength > 0)
        {
            var imageBits = new byte[streamLength];
            memory.Seek (0, SeekOrigin.Begin);
            memory.Read (imageBits, 0, streamLength).NotUsed();
            memory.Dispose();

            var height = Image.PixelHeight;
            var width = Image.PixelWidth;

            if (ReadWord (imageBits, 0) != 0x4d42 || // "BM"
                ReadDWord (imageBits, 2) != streamLength ||
                ReadDWord (imageBits, 14) != 40 || // sizeof BITMAPINFOHEADER
                ReadDWord (imageBits, 18) != width ||
                ReadDWord (imageBits, 22) != height)
            {
                throw new NotImplementedException ("ReadIndexedMemoryBitmap: unsupported format");
            }

            var fileBits = ReadWord (imageBits, 28);
            if (fileBits != bits)
            {
                if (fileBits == 1 || fileBits == 4 || fileBits == 8)
                    bits = fileBits;
            }

            if (ReadWord (imageBits, 26) != 1 ||
                ReadWord (imageBits, 28) != bits ||
                ReadDWord (imageBits, 30) != 0)
            {
                throw new NotImplementedException ("ReadIndexedMemoryBitmap: unsupported format #2");
            }

            var bytesFileOffset = ReadDWord (imageBits, 10);
            const int
                bytesColorPaletteOffset =
                    0x36; // GDI+ always returns Windows bitmaps: sizeof BITMAPFILEHEADER + sizeof BITMAPINFOHEADER
            var paletteColors = ReadDWord (imageBits, 46);
            if ((bytesFileOffset - bytesColorPaletteOffset) / 4 != paletteColors)
            {
                throw new NotImplementedException ("ReadIndexedMemoryBitmap: unsupported format #3");
            }

            var mask = new MonochromeMask (width, height);

            var isGray = bits == 8 && (paletteColors == 256 || paletteColors == 0);
            var isBitonal = 0; // 0: false; >0: true; <0: true (inverted)
            var paletteData = new byte[3 * paletteColors];
            for (var color = 0; color < paletteColors; ++color)
            {
                paletteData[3 * color] = imageBits[bytesColorPaletteOffset + 4 * color + 2];
                paletteData[3 * color + 1] = imageBits[bytesColorPaletteOffset + 4 * color + 1];
                paletteData[3 * color + 2] = imageBits[bytesColorPaletteOffset + 4 * color + 0];
                if (isGray)
                    isGray = paletteData[3 * color] == paletteData[3 * color + 1] &&
                             paletteData[3 * color] == paletteData[3 * color + 2];

                if (imageBits[bytesColorPaletteOffset + 4 * color + 3] < 128)
                {
                    // We treat this as transparency:
                    if (firstMaskColor == -1)
                        firstMaskColor = color;
                    if (lastMaskColor == -1 || lastMaskColor == color - 1)
                        lastMaskColor = color;
                    if (lastMaskColor != color)
                        segmentedColorMask = true;
                }

                //else
                //{
                //  // We treat this as opacity:
                //}
            }

            if (bits == 1)
            {
                if (paletteColors == 0)
                    isBitonal = 1;
                if (paletteColors == 2)
                {
                    if (paletteData[0] == 0 &&
                        paletteData[1] == 0 &&
                        paletteData[2] == 0 &&
                        paletteData[3] == 255 &&
                        paletteData[4] == 255 &&
                        paletteData[5] == 255)
                        isBitonal = 1; // Black on white
                    if (paletteData[5] == 0 &&
                        paletteData[4] == 0 &&
                        paletteData[3] == 0 &&
                        paletteData[2] == 255 &&
                        paletteData[1] == 255 &&
                        paletteData[0] == 255)
                        isBitonal = -1; // White on black
                }
            }

            // NYI: (no sample found where this was required)
            // if (segmentedColorMask = true)
            // { ... }

            var isFaxEncoding = false;
            var imageData = new byte[((width * bits + 7) / 8) * height];
            byte[]? imageDataFax = null;
            var k = 0;

            if (bits == 1)
            {
                // TODO: flag/option?
                // We try Group 3 1D and Group 4 (2D) encoding here and keep the smaller byte array.
                //byte[] temp = new byte[imageData.Length];
                //int ccittSize = DoFaxEncoding(ref temp, imageBits, (uint)bytesFileOffset, (uint)width, (uint)height);

                // It seems that Group 3 2D encoding never beats both other encodings, therefore we don't call it here.
                //byte[] temp2D = new byte[imageData.Length];
                //uint dpiY = (uint)image.VerticalResolution;
                //uint kTmp = 0;
                //int ccittSize2D = DoFaxEncoding2D((uint)bytesFileOffset, ref temp2D, imageBits, (uint)width, (uint)height, dpiY, out kTmp);
                //k = (int) kTmp;

                var tempG4 = new byte[imageData.Length];
                var ccittSizeG4 = DoFaxEncodingGroup4 (ref tempG4, imageBits, (uint)bytesFileOffset, (uint)width,
                    (uint)height);

                isFaxEncoding = /*ccittSize > 0 ||*/ ccittSizeG4 > 0;
                if (isFaxEncoding)
                {
                    //if (ccittSize == 0)
                    //  ccittSize = 0x7fffffff;
                    if (ccittSizeG4 == 0)
                        ccittSizeG4 = 0x7fffffff;

                    //if (ccittSize <= ccittSizeG4)
                    //{
                    //  Array.Resize(ref temp, ccittSize);
                    //  imageDataFax = temp;
                    //  k = 0;
                    //}
                    //else
                    {
                        Array.Resize (ref tempG4, ccittSizeG4);
                        imageDataFax = tempG4;
                        k = -1;
                    }
                }
            }

            //if (!isFaxEncoding)
            {
                var bytesOffsetRead = 0;
                if (bits == 8 || bits == 4 || bits == 1)
                {
                    var bytesPerLine = (width * bits + 7) / 8;
                    for (var y = 0; y < height; ++y)
                    {
                        mask.StartLine (y);
                        var bytesOffsetWrite = (height - 1 - y) * ((width * bits + 7) / 8);
                        for (var x = 0; x < bytesPerLine; ++x)
                        {
                            if (isGray)
                            {
                                // Lookup the gray value from the palette:
                                imageData[bytesOffsetWrite] =
                                    paletteData[3 * imageBits[bytesFileOffset + bytesOffsetRead]];
                            }
                            else
                            {
                                // Store the palette index.
                                imageData[bytesOffsetWrite] = imageBits[bytesFileOffset + bytesOffsetRead];
                            }

                            if (firstMaskColor != -1)
                            {
                                int n = imageBits[bytesFileOffset + bytesOffsetRead];
                                if (bits == 8)
                                {
                                    // TODO???: segmentedColorMask == true => bad mask NYI
                                    mask.AddPel ((n >= firstMaskColor) && (n <= lastMaskColor));
                                }
                                else if (bits == 4)
                                {
                                    // TODO???: segmentedColorMask == true => bad mask NYI
                                    var n1 = (n & 0xf0) / 16;
                                    var n2 = (n & 0x0f);
                                    mask.AddPel ((n1 >= firstMaskColor) && (n1 <= lastMaskColor));
                                    mask.AddPel ((n2 >= firstMaskColor) && (n2 <= lastMaskColor));
                                }
                                else if (bits == 1)
                                {
                                    // TODO???: segmentedColorMask == true => bad mask NYI
                                    for (var bit = 1; bit <= 8; ++bit)
                                    {
                                        var n1 = (n & 0x80) / 128;
                                        mask.AddPel ((n1 >= firstMaskColor) && (n1 <= lastMaskColor));
                                        n *= 2;
                                    }
                                }
                            }

                            bytesOffsetRead += 1;
                            bytesOffsetWrite += 1;
                        }

                        bytesOffsetRead = 4 * ((bytesOffsetRead + 3) / 4); // Align to 32 bit boundary
                    }
                }
                else
                {
                    throw new NotImplementedException ("ReadIndexedMemoryBitmap: unsupported format #3");
                }
            }

            var fd = new FlateDecode();
            if (firstMaskColor != -1 &&
                lastMaskColor != -1)
            {
                // Color mask requires Reader 4.0 or higher:
                //if (!segmentedColorMask && pdfVersion >= 13)
                if (!segmentedColorMask && pdfVersion >= 13 && !isGray)
                {
                    var array = new PdfArray (_document!);
                    array.Elements.Add (new PdfInteger (firstMaskColor));
                    array.Elements.Add (new PdfInteger (lastMaskColor));
                    Elements[Keys.Mask] = array;
                }
                else
                {
                    // Monochrome mask
                    var maskDataCompressed = fd.Encode (mask.MaskData, _document!.Options.FlateEncodeMode);
                    var pdfMask = new PdfDictionary (_document);
                    pdfMask.Elements.SetName (Keys.Type, "/XObject");
                    pdfMask.Elements.SetName (Keys.Subtype, "/Image");

                    Owner._irefTable.Add (pdfMask);
                    pdfMask.Stream = new PdfStream (maskDataCompressed, pdfMask);
                    pdfMask.Elements[PdfStream.Keys.Length] = new PdfInteger (maskDataCompressed.Length);
                    pdfMask.Elements[PdfStream.Keys.Filter] = new PdfName ("/FlateDecode");
                    pdfMask.Elements[Keys.Width] = new PdfInteger (width);
                    pdfMask.Elements[Keys.Height] = new PdfInteger (height);
                    pdfMask.Elements[Keys.BitsPerComponent] = new PdfInteger (1);
                    pdfMask.Elements[Keys.ImageMask] = new PdfBoolean (true);
                    Elements[Keys.Mask] = pdfMask.Reference;
                }
            }

            var imageDataCompressed = fd.Encode (imageData, _document!.Options.FlateEncodeMode);
            var imageDataFaxCompressed =
                isFaxEncoding ? fd.Encode (imageDataFax!, _document.Options.FlateEncodeMode) : null;

            var usesCcittEncoding = false;
            if (isFaxEncoding &&
                (imageDataFax!.Length < imageDataCompressed.Length ||
                 imageDataFaxCompressed!.Length < imageDataCompressed.Length))
            {
                // /CCITTFaxDecode creates the smaller file (with or without /FlateDecode):
                usesCcittEncoding = true;

                if (imageDataFax.Length < imageDataCompressed.Length)
                {
                    Stream = new PdfStream (imageDataFax, this);
                    Elements[PdfStream.Keys.Length] = new PdfInteger (imageDataFax.Length);
                    Elements[PdfStream.Keys.Filter] = new PdfName ("/CCITTFaxDecode");

                    //PdfArray array2 = new PdfArray(_document);
                    var dictionary = new PdfDictionary();
                    if (k != 0)
                        dictionary.Elements.Add ("/K", new PdfInteger (k));
                    if (isBitonal < 0)
                        dictionary.Elements.Add ("/BlackIs1", new PdfBoolean (true));
                    dictionary.Elements.Add ("/EndOfBlock", new PdfBoolean (false));
                    dictionary.Elements.Add ("/Columns", new PdfInteger (width));
                    dictionary.Elements.Add ("/Rows", new PdfInteger (height));

                    //array2.Elements.Add(dictionary);
                    Elements[PdfStream.Keys.DecodeParms] = dictionary; // array2;
                }
                else
                {
                    Stream = new PdfStream (imageDataFaxCompressed!, this);
                    Elements[PdfStream.Keys.Length] = new PdfInteger (imageDataFaxCompressed.Length);
                    var arrayFilters = new PdfArray (_document);
                    arrayFilters.Elements.Add (new PdfName ("/FlateDecode"));
                    arrayFilters.Elements.Add (new PdfName ("/CCITTFaxDecode"));
                    Elements[PdfStream.Keys.Filter] = arrayFilters;
                    var arrayDecodeParms = new PdfArray (_document);

                    var dictFlateDecodeParms = new PdfDictionary();

                    //dictFlateDecodeParms.Elements.Add("/Columns", new PdfInteger(1));

                    var dictCcittFaxDecodeParms = new PdfDictionary();
                    if (k != 0)
                        dictCcittFaxDecodeParms.Elements.Add ("/K", new PdfInteger (k));
                    if (isBitonal < 0)
                        dictCcittFaxDecodeParms.Elements.Add ("/BlackIs1", new PdfBoolean (true));
                    dictCcittFaxDecodeParms.Elements.Add ("/EndOfBlock", new PdfBoolean (false));
                    dictCcittFaxDecodeParms.Elements.Add ("/Columns", new PdfInteger (width));
                    dictCcittFaxDecodeParms.Elements.Add ("/Rows", new PdfInteger (height));

                    arrayDecodeParms.Elements.Add (dictFlateDecodeParms); // How to add the "null object"?
                    arrayDecodeParms.Elements.Add (dictCcittFaxDecodeParms);
                    Elements[PdfStream.Keys.DecodeParms] = arrayDecodeParms;
                }
            }
            else
            {
                // /FlateDecode creates the smaller file (or no monochrome bitmap):
                Stream = new PdfStream (imageDataCompressed, this);
                Elements[PdfStream.Keys.Length] = new PdfInteger (imageDataCompressed.Length);
                Elements[PdfStream.Keys.Filter] = new PdfName ("/FlateDecode");
            }

            Elements[Keys.Width] = new PdfInteger (width);
            Elements[Keys.Height] = new PdfInteger (height);
            Elements[Keys.BitsPerComponent] = new PdfInteger (bits);

            // TODO: CMYK

            // CCITT encoding: we need color palette for isBitonal == 0
            // FlateDecode: we need color palette for isBitonal <= 0 unless we have grayscales
            if ((usesCcittEncoding && isBitonal == 0) ||
                (!usesCcittEncoding && isBitonal <= 0 && !isGray))
            {
                var colorPalette = new PdfDictionary (_document);
                var packedPaletteData = paletteData.Length >= 48
                    ? fd.Encode (paletteData, _document.Options.FlateEncodeMode)
                    : null; // don't compress small palettes
                if (packedPaletteData != null &&
                    packedPaletteData.Length + 20 <
                    paletteData.Length) // +20: compensate for the overhead (estimated value)
                {
                    // Create compressed color palette:
                    colorPalette.CreateStream (packedPaletteData);
                    colorPalette.Elements[PdfStream.Keys.Length] = new PdfInteger (packedPaletteData.Length);
                    colorPalette.Elements[PdfStream.Keys.Filter] = new PdfName ("/FlateDecode");
                }
                else
                {
                    // Create uncompressed color palette:
                    colorPalette.CreateStream (paletteData);
                    colorPalette.Elements[PdfStream.Keys.Length] = new PdfInteger (paletteData.Length);
                }

                Owner._irefTable.Add (colorPalette);

                var arrayColorSpace = new PdfArray (_document);
                arrayColorSpace.Elements.Add (new PdfName ("/Indexed"));
                arrayColorSpace.Elements.Add (new PdfName ("/DeviceRGB"));
                arrayColorSpace.Elements.Add (new PdfInteger (paletteColors - 1));
                arrayColorSpace.Elements.Add (colorPalette.Reference!);
                Elements[Keys.ColorSpace] = arrayColorSpace;
            }
            else
            {
                Elements[Keys.ColorSpace] = new PdfName ("/DeviceGray");
            }

            if (Image.Interpolate)
                Elements[Keys.Interpolate] = PdfBoolean.True;
        }
    }

    /// <summary>
    /// Common keys for all streams.
    /// </summary>
    public new sealed class Keys : PdfXObject.Keys
    {
        // ReSharper disable InconsistentNaming

        /// <summary>
        /// (Optional) The type of PDF object that this dictionary describes;
        /// if present, must be XObject for an image XObject.
        /// </summary>
        [KeyInfo (KeyType.Name | KeyType.Optional)]
        public const string Type = "/Type";

        /// <summary>
        /// (Required) The type of XObject that this dictionary describes;
        /// must be Image for an image XObject.
        /// </summary>
        [KeyInfo (KeyType.Name | KeyType.Required)]
        public const string Subtype = "/Subtype";

        /// <summary>
        /// (Required) The width of the image, in samples.
        /// </summary>
        [KeyInfo (KeyType.Integer | KeyType.Required)]
        public const string Width = "/Width";

        /// <summary>
        /// (Required) The height of the image, in samples.
        /// </summary>
        [KeyInfo (KeyType.Integer | KeyType.Required)]
        public const string Height = "/Height";

        /// <summary>
        /// (Required for images, except those that use the JPXDecode filter; not allowed for image masks)
        /// The color space in which image samples are specified; it can be any type of color space except
        /// Pattern. If the image uses the JPXDecode filter, this entry is optional:
        /// � If ColorSpace is present, any color space specifications in the JPEG2000 data are ignored.
        /// � If ColorSpace is absent, the color space specifications in the JPEG2000 data are used.
        ///   The Decode array is also ignored unless ImageMask is true.
        /// </summary>
        [KeyInfo (KeyType.NameOrArray | KeyType.Required)]
        public const string ColorSpace = "/ColorSpace";

        /// <summary>
        /// (Required except for image masks and images that use the JPXDecode filter)
        /// The number of bits used to represent each color component. Only a single value may be specified;
        /// the number of bits is the same for all color components. Valid values are 1, 2, 4, 8, and
        /// (in PDF 1.5) 16. If ImageMask is true, this entry is optional, and if specified, its value
        /// must be 1.
        /// If the image stream uses a filter, the value of BitsPerComponent must be consistent with the
        /// size of the data samples that the filter delivers. In particular, a CCITTFaxDecode or JBIG2Decode
        /// filter always delivers 1-bit samples, a RunLengthDecode or DCTDecode filter delivers 8-bit samples,
        /// and an LZWDecode or FlateDecode filter delivers samples of a specified size if a predictor function
        /// is used.
        /// If the image stream uses the JPXDecode filter, this entry is optional and ignored if present.
        /// The bit depth is determined in the process of decoding the JPEG2000 image.
        /// </summary>
        [KeyInfo (KeyType.Integer | KeyType.Required)]
        public const string BitsPerComponent = "/BitsPerComponent";

        /// <summary>
        /// (Optional; PDF 1.1) The name of a color rendering intent to be used in rendering the image.
        /// Default value: the current rendering intent in the graphics state.
        /// </summary>
        [KeyInfo (KeyType.Name | KeyType.Optional)]
        public const string Intent = "/Intent";

        /// <summary>
        /// (Optional) A flag indicating whether the image is to be treated as an image mask.
        /// If this flag is true, the value of BitsPerComponent must be 1 and Mask and ColorSpace should
        /// not be specified; unmasked areas are painted using the current nonstroking color.
        /// Default value: false.
        /// </summary>
        [KeyInfo (KeyType.Boolean | KeyType.Optional)]
        public const string ImageMask = "/ImageMask";

        /// <summary>
        /// (Optional except for image masks; not allowed for image masks; PDF 1.3)
        /// An image XObject defining an image mask to be applied to this image, or an array specifying
        /// a range of colors to be applied to it as a color key mask. If ImageMask is true, this entry
        /// must not be present.
        /// </summary>
        [KeyInfo (KeyType.StreamOrArray | KeyType.Optional)]
        public const string Mask = "/Mask";

        /// <summary>
        /// (Optional) An array of numbers describing how to map image samples into the range of values
        /// appropriate for the image�s color space. If ImageMask is true, the array must be either
        /// [0 1] or [1 0]; otherwise, its length must be twice the number of color components required
        /// by ColorSpace. If the image uses the JPXDecode filter and ImageMask is false, Decode is ignored.
        /// Default value: see �Decode Arrays�.
        /// </summary>
        [KeyInfo (KeyType.Array | KeyType.Optional)]
        public const string Decode = "/Decode";

        /// <summary>
        /// (Optional) A flag indicating whether image interpolation is to be performed.
        /// Default value: false.
        /// </summary>
        [KeyInfo (KeyType.Boolean | KeyType.Optional)]
        public const string Interpolate = "/Interpolate";

        /// <summary>
        /// (Optional; PDF 1.3) An array of alternate image dictionaries for this image. The order of
        /// elements within the array has no significance. This entry may not be present in an image
        /// XObject that is itself an alternate image.
        /// </summary>
        [KeyInfo (KeyType.Array | KeyType.Optional)]
        public const string Alternates = "/Alternates";

        /// <summary>
        /// (Optional; PDF 1.4) A subsidiary image XObject defining a soft-mask image to be used as a
        /// source of mask shape or mask opacity values in the transparent imaging model. The alpha
        /// source parameter in the graphics state determines whether the mask values are interpreted as
        /// shape or opacity. If present, this entry overrides the current soft mask in the graphics state,
        /// as well as the image�s Mask entry, if any. (However, the other transparency related graphics
        /// state parameters � blend mode and alpha constant � remain in effect.) If SMask is absent, the
        /// image has no associated soft mask (although the current soft mask in the graphics state may
        /// still apply).
        /// </summary>
        [KeyInfo (KeyType.Integer | KeyType.Required)]
        public const string SMask = "/SMask";

        /// <summary>
        /// (Optional for images that use the JPXDecode filter, meaningless otherwise; PDF 1.5)
        /// A code specifying how soft-mask information encoded with image samples should be used:
        /// 0 If present, encoded soft-mask image information should be ignored.
        /// 1 The image�s data stream includes encoded soft-mask values. An application can create
        ///   a soft-mask image from the information to be used as a source of mask shape or mask
        ///   opacity in the transparency imaging model.
        /// 2 The image�s data stream includes color channels that have been preblended with a
        ///   background; the image data also includes an opacity channel. An application can create
        ///   a soft-mask image with a Matte entry from the opacity channel information to be used as
        ///   a source of mask shape or mask opacity in the transparency model. If this entry has a
        ///   nonzero value, SMask should not be specified.
        /// Default value: 0.
        /// </summary>
        [KeyInfo (KeyType.Integer | KeyType.Optional)]
        public const string SMaskInData = "/SMaskInData";

        /// <summary>
        /// (Required in PDF 1.0; optional otherwise) The name by which this image XObject is
        /// referenced in the XObject subdictionary of the current resource dictionary.
        /// </summary>
        [KeyInfo (KeyType.Name | KeyType.Optional)]
        public const string Name = "/Name";

        /// <summary>
        /// (Required if the image is a structural content item; PDF 1.3) The integer key of the
        /// image�s entry in the structural parent tree.
        /// </summary>
        [KeyInfo (KeyType.Integer | KeyType.Required)]
        public const string StructParent = "/StructParent";

        /// <summary>
        /// (Optional; PDF 1.3; indirect reference preferred) The digital identifier of the image�s
        /// parent Web Capture content set.
        /// </summary>
        [KeyInfo (KeyType.String | KeyType.Optional)]
        public const string ID = "/ID";

        /// <summary>
        /// (Optional; PDF 1.2) An OPI version dictionary for the image. If ImageMask is true,
        /// this entry is ignored.
        /// </summary>
        [KeyInfo (KeyType.Dictionary | KeyType.Optional)]
        public const string OPI = "/OPI";

        /// <summary>
        /// (Optional; PDF 1.4) A metadata stream containing metadata for the image.
        /// </summary>
        [KeyInfo (KeyType.Stream | KeyType.Optional)]
        public const string Metadata = "/Metadata";

        /// <summary>
        /// (Optional; PDF 1.5) An optional content group or optional content membership dictionary,
        /// specifying the optional content properties for this image XObject. Before the image is
        /// processed, its visibility is determined based on this entry. If it is determined to be
        /// invisible, the entire image is skipped, as if there were no Do operator to invoke it.
        /// </summary>
        [KeyInfo (KeyType.Dictionary | KeyType.Optional)]
        public const string OC = "/OC";

        // ReSharper restore InconsistentNaming
    }
}

/// <summary>
/// Helper class for creating bitmap masks (8 pels per byte).
/// </summary>
class MonochromeMask
{
    /// <summary>
    /// Returns the bitmap mask that will be written to PDF.
    /// </summary>
    public byte[] MaskData => _maskData;

    private readonly byte[] _maskData;

    /// <summary>
    /// Creates a bitmap mask.
    /// </summary>
    public MonochromeMask (int sizeX, int sizeY)
    {
        _sizeX = sizeX;
        _sizeY = sizeY;
        var byteSize = ((sizeX + 7) / 8) * sizeY;
        _maskData = new byte[byteSize];
        StartLine (0);
    }

    /// <summary>
    /// Starts a new line.
    /// </summary>
    public void StartLine (int newCurrentLine)
    {
        _bitsWritten = 0;
        _byteBuffer = 0;
        _writeOffset = ((_sizeX + 7) / 8) * (_sizeY - 1 - newCurrentLine);
    }

    /// <summary>
    /// Adds a pel to the current line.
    /// </summary>
    /// <param name="isTransparent"></param>
    public void AddPel (bool isTransparent)
    {
        if (_bitsWritten < _sizeX)
        {
            // Mask: 0: opaque, 1: transparent (default mapping)
            if (isTransparent)
                _byteBuffer = (_byteBuffer << 1) + 1;
            else
                _byteBuffer = _byteBuffer << 1;
            ++_bitsWritten;
            if ((_bitsWritten & 7) == 0)
            {
                _maskData[_writeOffset] = (byte)_byteBuffer;
                ++_writeOffset;
                _byteBuffer = 0;
            }
            else if (_bitsWritten == _sizeX)
            {
                var n = 8 - (_bitsWritten & 7);
                _byteBuffer = _byteBuffer << n;
                _maskData[_writeOffset] = (byte)_byteBuffer;
            }
        }
    }

    /// <summary>
    /// Adds a pel from an alpha mask value.
    /// </summary>
    public void AddPel (int shade)
    {
        // NYI: dithering!!!
        AddPel (shade < 128);
    }

    private readonly int _sizeX;
    private readonly int _sizeY;
    private int _writeOffset;
    private int _byteBuffer;
    private int _bitsWritten;
}
